package scu.maqiang.neuralnetwork;

import scu.maqiang.neuralnetwork.math.IActivationFunction;
import scu.maqiang.neuralnetwork.math.RandomNumberGenerator;

import java.util.ArrayList;
import java.util.Random;

/**
 * 神经元类
 */

import java.util.ArrayList;


/**
 * Neuron
 * 这个类表示人工神经元, 在神经网格中是最基本的单元。它封闭了所有与神经元相关的属性, 包括权重, 输入, 输出, 激活函数与计算。
 * 对于bias一个重要的点是: 极重从0到输入的总个数编号, 因为要包括最后的权重来乘以bias.
 *
 * @author Alan de Souza, F谩bio Soares
 * @version 0.1
 */
public class Neuron {

    /**
     * 与神经元相关的权重
     */
    protected ArrayList<Double> weight;
    /**
     * 神经元的输入
     */
    private ArrayList<Double> input;
    /**
     * 神经元的输出, 由激活函数activationfunction产生
     */
    private Double output;
    /**
     * 传入激活函数的值.
     */
    private Double outputBeforeActivation;

    /**
     * 输入个数, 如果为0, 则表示还未初始化。
     */
    private int numberOfInputs = 0;

    /**
     * 神经元的基, 应总是1.0, 除了在第一层。
     */
    protected Double bias = 1.0;

    /**
     * 该神经元的激活函数
     */
    private IActivationFunction activationFunction;


    /**
     * 默认构造函数
     */
    public Neuron() {

    }

    /**
     * 构造函数， 传入输入数据的个数
     *
     * @param numberofinputs Number of Inputs
     */
    public Neuron(int numberofinputs) {
        numberOfInputs = numberofinputs;
        weight = new ArrayList<>(numberofinputs + 1);
        input = new ArrayList<>(numberofinputs);
    }

    /**
     * 构造函数, 输入输入数据的个数与激活函数
     *
     * @param numberofinputs 输入数据个数
     * @param iaf            激活函数
     */
    public Neuron(int numberofinputs, IActivationFunction iaf) {
        numberOfInputs = numberofinputs;
        weight = new ArrayList<>(numberofinputs + 1);
        input = new ArrayList<>(numberofinputs);
        activationFunction = iaf;
    }

    /**
     * 初始化
     * 该方法通过随机设计权重来初始化神经元。
     */
    public void init() {
        if (numberOfInputs > 0) {
            for (int i = 0; i <= numberOfInputs; i++) {
                double newWeight = RandomNumberGenerator.GenerateNext();
                try {
                    this.weight.set(i, newWeight);
                } catch (IndexOutOfBoundsException iobe) {
                    this.weight.add(newWeight);
                }
            }
        }
    }


    /**
     * setInputs
     * Sets a vector of double-precision values to the neuron input
     *
     * @param values vector of values applied at the neuron input
     */
    public void setInputs(double[] values) {
        if (values.length == numberOfInputs) {
            for (int i = 0; i < numberOfInputs; i++) {
                try {
                    input.set(i, values[i]);
                } catch (IndexOutOfBoundsException iobe) {
                    input.add(values[i]);
                }
            }
        }
    }

    /**
     * setInputs
     * Sets an array of values to the neuron's input
     *
     * @param values
     */
    public void setInputs(ArrayList<Double> values) {
        if (values.size() == numberOfInputs) {
            input = values;
        }
    }

    /**
     * getArrayInputs
     *
     * @return Returns the neuron's inputs in an ArrayList
     */
    public ArrayList<Double> getArrayInputs() {
        return input;
    }

    /**
     * getInputs
     *
     * @return Return the neuron's inputs in a vector
     */
    public double[] getInputs() {
        double[] inputs = new double[numberOfInputs];
        for (int i = 0; i < numberOfInputs; i++) {
            inputs[i] = this.input.get(i);
        }
        return inputs;
    }

    /**
     * setInput
     * Sets a real value at the ith java position of the neuron's inputs
     *
     * @param i     neuron input java index
     * @param value value to be set in the input
     */
    public void setInput(int i, double value) {
        if (i >= 0 && i < numberOfInputs) {
            try {
                input.set(i, value);
            } catch (IndexOutOfBoundsException iobe) {
                input.add(value);
            }
        }
    }

    /**
     * getInput
     *
     * @param i ith java position at the input
     * @return Returns the ith java input
     */
    public double getInput(int i) {
        return input.get(i);
    }

    /**
     * getWeights
     *
     * @return Returns the neuron's weights in the form of vector
     */
    public double[] getWeights() {
        double[] weights = new double[numberOfInputs + 1];
        for (int i = 0; i <= numberOfInputs; i++) {
            weights[i] = weight.get(i);
        }
        return weights;
    }

    /**
     * getArrayWeights
     *
     * @return Returns the neuron's weights in the form of Arraylist
     */
    public ArrayList<Double> getArrayWeights() {
        return weight;
    }

    /**
     * updateWeight
     * Method used for updating the weight during learning
     *
     * @param i     ith java position of the weight
     * @param value value to be updated on the weight
     */
    public void updateWeight(int i, double value) {
        if (i >= 0 && i <= numberOfInputs) {
            weight.set(i, value);
        }
    }

    /**
     * getNumberOfInputs
     *
     * @return Returns the number of inputs
     */
    public int getNumberOfInputs() {
        return this.numberOfInputs;
    }

    /**
     * setWeight
     * sets the weight at the ith java position
     *
     * @param i     ith java position
     * @param value value to be set on the weight
     * @throws NeuralException
     */
    public void setWeight(int i, double value) throws NeuralException {
        if (i >= 0 && i < numberOfInputs) {
            this.weight.set(i, value);
        } else {
            throw new NeuralException("Invalid weight index");
        }
    }

    /**
     * getOutput
     *
     * @return Returns the neuron's output
     */
    public double getOutput() {
        return output;
    }

    /**
     * calc
     * Calculates the neuron's output
     */
    public void calc() {
        outputBeforeActivation = 0.0;
        if (numberOfInputs > 0) {
            if (input != null && weight != null) {
//                for (int i = 0; i <= numberOfInputs; i++) {
//                    outputBeforeActivation += (i == numberOfInputs ? bias : input.get(i)) * weight.get(i);
//                }
                for(int i = 0; i < numberOfInputs; i++) {
                    outputBeforeActivation += input.get(i) * weight.get(i);
                }
                outputBeforeActivation += bias * weight.get(numberOfInputs);
            }
        }
        output = activationFunction.calc(outputBeforeActivation);
    }

    /**
     * 设置激活函数
     * 设置这个神经元的激活函数
     *
     * @param iaf 激活函数
     */
    public void setActivationFunction(IActivationFunction iaf) {
        this.activationFunction = iaf;
    }

    /**
     * getOutputBeforeActivation
     *
     * @return Returns the weighted sum of the inputs multiplied by weights
     */
    public double getOutputBeforeActivation() {
        return outputBeforeActivation;
    }


}


